package no.ffi.gismoidm.pubsub;
// IdmCore
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.Socket;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignedObject;
import java.util.ArrayList;
import java.util.Collections;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SealedObject;
import javax.crypto.SecretKey;
import no.ffi.gismoidm.idp.Config;
import no.ffi.gismoidm.invocations.ServiceInvocation;
import no.ffi.gismoidm.invocations.ServiceInvocationFactory;
import no.ffi.gismoidm.invocations.ServiceInvocationStateless;
import no.ffi.gismoidm.utils.IdentityStatement;
import no.ffi.gismoidm.utils.GismoIdmException;
import no.ffi.gismoidm.utils.SealedObjectReplacement;
import no.ffi.tpm.crypto.CryptoOperations;
import no.fongen.os.FIFO;

/**
 * Client proxy for authentication, subscription and
 * notification of publications. It also contains a thread
 * which manages the renewal of identity statements.
 * @author anders
 */
public class PubSubClientProxy implements PublicationListener {
    IdentityStatement hisIS;
    PublicKey trustAnchor;
    SecretKey sessionKey;
    CryptoOperations myCrOp;
    Socket s;
    ObjectInputStream ois;
    ObjectOutputStream oos;
    PublicationListener publicationListener;
    String mrID; // subject name of connected message router
    boolean online;
    String ipAddr;
    int tcpPort;
    ArrayList subscriptions;
    MyCredentials myCredentials;

    FIFO outwardMessages;    

    public synchronized void setOnline() {
        System.out.println("Set Online");
        online = true;
        notifyAll();
    }
    
    public synchronized void setOffline() {
        System.out.println("Set Offline");
        online = false;
    }
    
    public synchronized void waitUntilOnline() {
        System.out.println("Waiting until online..");
        while (!online) {
            try { wait(); } catch (Exception e) {
            System.out.println("Exception during thread sleep");}
        }
        System.out.println("ok");
    }
    
    
    public static void main(String[] args) throws Exception {
        if (args.length > 0)
            new PubSubClientProxy(args[0]).TestCode();
        else
            new PubSubClientProxy("/home/anders/config/AndersFongen.properties").TestCode();
    }
    
    public PubSubClientProxy(String configFilename) throws Exception {
        // Find certificate, keys and identity statements
        Config.loadPropertiesFrom(configFilename);
        myCredentials = MyCredentials.getInstance();
        myCrOp = myCredentials.getCryptoOperator();
        IdentityStatement myIS = myCredentials.getIdentityStatement();
        trustAnchor = myCredentials.getTrustAnchor();
        
        System.out.println(myIS.getAttributeAsProperties());
        
        subscriptions = new ArrayList();
        
        outwardMessages = new FIFO(10);
        myCredentials.addToIsDistribution(outwardMessages);
        
        mrID = Config.getProperty("MESSAGEROUTER_ID");
        ipAddr = Config.getProperty("MESSAGEROUTER_IP_ADDRESS", "localhost");
        tcpPort = Integer.parseInt(Config.getProperty("MESSAGEROUTER_TCP_PORT","1566"));
        
          while (true) {
              try {
                connectToMessageRouter(ipAddr, tcpPort, mrID);
                break;
              } catch (Exception ex2) {
                System.out.println("Error during connect-1:" + ex2.getMessage());
                try { Thread.sleep(10000);} catch (Exception e3) {}
              }
          }

                      // Start listener thread
        setPublicationListener(this);
        new Listener();
        new Sender();
    }
    
    public void TestCode() throws Exception {
        // TODO - TEST CODE: remove after testing
//        System.out.println("Klart for å sende en subscription");
//        BooleanOperand sreq1 = new Equals(new AttributeOperand("NATIONALITY"), new StringOperand("NO"));
//        BooleanOperand sreq2 = new Equals(new AttributeOperand("CLEARANCE"),new StringOperand("SECRET"));
//        BooleanOperand sreq3 = new GreaterThan(new AttributeOperand("BUILDING"), new IntegerOperand("199"));
//        BooleanOperand sreq = new Or(new Or(sreq1,sreq3),sreq2);//new Or(sreq1, sreq2));
//        
//        BooleanOperand preq = new Or(sreq1,sreq2);
//        IdentityStatement myIS = myCredentials.getIdentityStatement();
//        System.out.println(preq.getBooleanValue(myIS.getAttributeAsProperties()));
//        System.out.println(sreq.getBooleanValue(myIS.getAttributeAsProperties()));
//        
//        
        Subscription sub = new Subscription(new SubscriptionTopic("/"), null);
        
        setSubscription(sub);
        setPublicationListener(this);
//        Publication pub = new Publication("abcdef",Collections.singletonList("/nyheter/norge"),sreq2,myCrOp);
//        sendPublication(pub);
//        //pub = new Publication(""+Math.random(), "/urgency/question");
//        sendPublication(pub);
        Thread.sleep(Integer.MAX_VALUE);
    }
    
    
    public CryptoOperations getCryptoOperations() { return myCrOp; }
    
    public void connectToMessageRouter(String endpoint, int port, String mrID) throws Exception {
       // Create a ServiceInvocation object
        ServiceInvocationFactory sif = new ServiceInvocationFactory(ServiceInvocationFactory.STATELESS);
        IdentityStatement myIS = myCredentials.getIdentityStatement();
        ServiceInvocation sio = sif.createRequestInstance("",myIS,myCrOp, 0);
       
        try {
            // Connect to resource and construct stream objects
            s = new Socket(endpoint,port);
            oos = new ObjectOutputStream(s.getOutputStream());
            ois = new ObjectInputStream(s.getInputStream());
        } catch (Exception e) { 
            System.out.println("MR connection unsuccessful, I try again.");
            throw new GismoIdmException("Connection not successful: " + e.getMessage());
        }

        // Exceptions during the next lines are related to
        // keys, certificates, trustanchros etc. and are not
        // recoverable. THey are passed up the call chaing

        // Pass the ServiceInvocation object to the MR
        oos.writeObject(sio);

        // Read the response, or a socket disconnect
        Object o = ois.readObject();
        if (o instanceof ServiceInvocation) {
            ServiceInvocation sii = (ServiceInvocation)o;
            sii.validateResponse(sio.getNonce(),null,mrID,null, myCrOp);
            sessionKey = ((ServiceInvocationStateless)o).getSecretKey();
            hisIS = sii.getIdentityStatement();
        }
        setOnline();
        if (!subscriptions.isEmpty())
            setSubscriptions(subscriptions);
    }
    
    public void setSubscription(String topic) throws Exception {
        setSubscription(new Subscription(topic,null));
    }
    
    public void setSubscription(Subscription subscr) throws Exception {
        subscriptions.clear();
        subscriptions.add(subscr);
        sendMessage((Serializable)subscr);
    }
    
    public void setSubscriptions(ArrayList<Subscription> subscr) throws Exception {
        subscriptions = (ArrayList)subscr.clone();
        sendMessage(subscr);
    }
    
    public void removeSubscription() throws Exception {
        Subscription subscr = new Subscription(true);
        sendMessage(subscr);
    }
    
    public void setPublicationListener(PublicationListener pl) throws Exception {
        publicationListener = pl;
        // start listener thread now? No, at once, since we need to receive ISes
    }
    public void sendPublication(Publication pub) throws Exception {
        sendMessage(pub);
    }
    
    
    public void sendMessage(Serializable msg) throws Exception {
        System.out.println("Putting outbound message in queue");
        outwardMessages.nonblockput(msg);
    }

    @Override
    public void publicationReceived(Publication pub) {
        System.out.println(pub);
        try {
            pub.verifySignature(trustAnchor);
            System.out.println(pub.getPublicationObject());
        } catch (Exception e) { e.printStackTrace(); }    
        
    }
    
    class Listener implements Runnable {
        
        public Listener() {
            Thread l = new Thread(this);
            l.setDaemon(true);
            l.start();
        }

        public void run() {
            // Listen for incoming messages. Process identity statements, and
            // forward other messages to publication listener
            while (true) {
                try {
                    Object o = ois.readObject();
                    System.out.println("---------MELDING MOTTATT-----------");
                    // Is this encrypted, then decrypt it
                    if (o instanceof SealedObjectReplacement) {
                        SealedObjectReplacement so = (SealedObjectReplacement)o;
                        Cipher cip = Cipher.getInstance(Config.SYMMETRIC_CRYPTO_ALGORITHM);
                        cip.init(Cipher.DECRYPT_MODE, sessionKey);
                        o = so.getObject(cip);
                        writeToFile(o);
                    }
                    if (o instanceof IdentityStatement) { // new IS received, validate it
                        IdentityStatement newIS = (IdentityStatement)o;
                        newIS.validateIdentityStatement(trustAnchor);
                        if (newIS.validateSubjectName(mrID)) // Same subjectName as last time
                            hisIS = newIS;
                    } else { // pass message to Neighbor object for processing
                        if (hisIS.identityStatementExpired(0)) { // IS is expired, terminate connection
                            s.close();
                            return; // will exit the server thread
                        } else { // Send to publication handler if possible
                            if (o instanceof Publication && publicationListener != null)
                                publicationListener.publicationReceived((Publication)o);
                        }
                    }
                } catch (ClassNotFoundException cnfe) { cnfe.printStackTrace(); }
                  catch (NoSuchAlgorithmException nsae) { nsae.printStackTrace(); }
                  catch (NoSuchPaddingException nspe) { nspe.printStackTrace(); }                
                  catch (BadPaddingException bpe) { bpe.printStackTrace(); }                
                  catch (IllegalBlockSizeException ibse) { ibse.printStackTrace(); }                
                  catch (InvalidKeyException ike) { ike.printStackTrace(); }
                  catch (GismoIdmException pe) { pe.printStackTrace(); }
                  catch (IOException ioex) {
                      setOffline();
                      while (true) {
                          try {
                            connectToMessageRouter(ipAddr, tcpPort, mrID);
                            break;
                          } catch (Exception ex2) {
                            System.out.println("Error during connect-2:" + ex2.getMessage());
                            try { Thread.sleep(10000);} catch (Exception e3) {}
                          }
                      }
                  }
            }// while
         } 
     }
    
    class Sender implements Runnable {
        Sender() { 
            Thread t = new Thread(this);
            t.setDaemon(true);
            t.start(); 
        }
        public void run() {
            try {
                while (true) {
                    Object message = outwardMessages.get();
                    // Check expiration of MR identity statement
                    if (hisIS.identityStatementExpired(0)) {
                        System.out.println("Identity Statement of Message Router");
                        System.out.println("is expired. Closing connection");
                        oos.close();
                        ois.close();
                        try  {s.close();} catch (Exception e) {}
                        return;
                    }
                    
                    // Encrypt message
                    Cipher cip = Cipher.getInstance(Config.SYMMETRIC_CRYPTO_ALGORITHM);
                    cip.init(Cipher.ENCRYPT_MODE, sessionKey);
                    SealedObject so = new SealedObject((Serializable)message, cip); // so is now AES encrypted.
                    // Now send it
                    oos.writeObject(so); // TODO catch SocketException and reconnect
                }
             } catch (Exception e) {
                 e.printStackTrace(); //TODO refine
             }
        }
    }
    
    private void writeToFile(Object o) {
        try {
            FileOutputStream fos = new FileOutputStream("/home/anders/pub"+System.currentTimeMillis());
            if (o instanceof Publication) {
                Publication p = (Publication)o;
                o = p.getPublicationObject();
            }
            if (o instanceof SignedObject) {
                SignedObject so = (SignedObject)o;
                o = so.getObject();
            }
            if (o instanceof byte[]) {
                byte[] b = (byte[])o;
                fos.write(b);
            } else {
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
                bw.write(o.toString());
                bw.close();
            }
            fos.close();
        } catch (Exception e) { e.printStackTrace(); }
    }
    
}
